package com.nadostar.extensions.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xml.utils.DefaultErrorHandler;
import org.apache.xpath.CachedXPathAPI;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.xml.sax.EntityResolver;

/**
 * {@link File} 또는 {@link InputStream}으로부터 {@link Document}를 생성하고, 생성된
 * {@link Document}를 이용하여 XML 정보를 검색하는 데 사용되는 클래스.
 * <p>
 * {@link Document} 생성 부분을 제외한 검색 API는 모두 deprecated되었으므로, 검색 위주 의 API를 사용하는
 * 경우에는 {@link JaxpXmlDataHandler} 또는{@link Dom4jXmlDataHandler} 를 사용할 것을 권장한다.
 * 
 * @author Web Platform Development Team
 * @version $Rev: 22490 $, $Date: 2009-10-06 12:17:51 +0900 (火, 06 10 2009) $
 */
public class XMLDOMUtil {
	protected static final Log LOG = LogFactory.getLog(XMLDOMUtil.class);

	/**
	 * XPath 구분의 빠른 검색을 위한 {@link CachedXPathAPI} {@link Map}
	 */
	private static final Map<Document, CachedXPathAPI> CACHED_XPATH_APIS = Collections
			.synchronizedMap(new WeakHashMap<Document, CachedXPathAPI>());

	/**
	 * JAXP attribute used to configure the schema language for validation.
	 */
	private static final String SCHEMA_LANGUAGE_ATTRIBUTE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";

	/**
	 * JAXP attribute value indicating the XSD schema language.
	 */
	private static final String XSD_SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema";

	/**
	 * {@link File}을 이용하여 {@link Document}를 생성한다. XML에 대한 유효성 검증을 수행하지 않는다.
	 * 
	 * @param file
	 *            XML 정보를 담고 있는 {@link File}
	 * @return Document
	 * @throws Exception
	 *             {@link Document} 생성 Exception
	 */
	public static Document loading(File file) throws Exception {
		InputStream input = new FileInputStream(file);
		try {
			return loading(input);
		} finally {
			IOUtils.closeQuietly(input);

			// for Klocwork
			try {
				input.close();
			} catch (IOException ioe) {
				ioe.getMessage();
			}
		}
	}

	/**
	 * {@link InputStream}을 이용하여 {@link Document}를 생성한다. XML에 대한 유효성 검증을 수행하지
	 * 않는다.
	 * 
	 * @param stream
	 *            XML 정보를 담고 있는 {@link InputStream}
	 * @return Document
	 * @throws Exception
	 *             {@link Document} 생성 Exception
	 */
	public static Document loading(InputStream stream) throws Exception {
		return loading(stream, true);
	}

	/**
	 * 파일 정보를 이용하여 {@link Document}를 생성한다. XML에 대한 유효성 검증을 수행하지 않는다.
	 * 
	 * @param location
	 *            XML 파일 경로
	 * @param loader
	 *            클래스 패스상의 자원을 검색하기 위한 {@link ClassLoader}
	 * @return Document
	 * @throws Exception
	 *             {@link Document} 생성 Exception
	 */
	public static Document loading(String location, ResourceLoader loader)
			throws Exception {
		File xmlFile = new File(location);

		if (xmlFile.exists()) {
			return loading(xmlFile);
		}
		if (loader.getResource(location) != null) {
			InputStream input = loader.getResourceAsStream(location);
			try {
				return loading(input);
			} finally {
				IOUtils.closeQuietly(input);

				// for Klocwork
				try {
					input.close();
				} catch (IOException ioe) {
					ioe.getMessage();
				}
			}
		}

		return null;
	}

	/**
	 * 파일 정보를 이용하여 {@link Document}를 생성한다. XML에 대한 유효성 검증을 수행하지 않는다.
	 * 
	 * @param stream
	 *            XML 정보를 담고 있는 {@link InputStream}
	 * @param namespaceAware
	 *            namespace 정보 활성화 상태
	 * @return document Document
	 * @throws Exception
	 *             {@link Document} 생성 Exception
	 */
	public static Document loading(InputStream stream, boolean namespaceAware)
			throws Exception {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		setAllowJavaEncoding(factory);
		factory.setNamespaceAware(namespaceAware);
		DocumentBuilder builder = factory.newDocumentBuilder();
		com.nadostar.extensions.util.Document document = new com.nadostar.extensions.util.Document(
				builder.parse(stream));

		CACHED_XPATH_APIS.put(document, new CachedXPathAPI());

		return document;
	}

	/**
	 * {@link InputStream}을 이용하여 {@link Document}를 생성한다. XML Schema 기반의 유효성 검증을
	 * 수행한다.
	 * 
	 * @param stream
	 *            XML 정보를 담고 있는 {@link InputStream}
	 * @param validate
	 *            XML의 유효성 검증
	 * @param resolver
	 *            스키마를 검증하고 얻어오는 {@link EntityResolver}
	 * @return document Document
	 * @throws Exception
	 *             {@link Document} 생성 Exception
	 */
	public static Document loading(InputStream stream, boolean validate,
			EntityResolver resolver) throws Exception {
		// stream 형태의 contents 를 읽어서, DOM 정보를 생성함
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		setAllowJavaEncoding(factory);

		if (validate) {
			factory.setValidating(validate);
			factory.setNamespaceAware(validate);
			factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
		}

		DocumentBuilder builder = factory.newDocumentBuilder();

		if (resolver != null) {
			builder.setEntityResolver(resolver);
		}

		builder.setErrorHandler(new DefaultErrorHandler());

		com.nadostar.extensions.util.Document document = new com.nadostar.extensions.util.Document(
				builder.parse(stream));

		CACHED_XPATH_APIS.put(document, new CachedXPathAPI());

		return document;
	}

	/**
	 * {@link Document}의 XML을 파일로 출력한다. 저장되는 XML의 인코딩은 기본 값인 utf-8로 설정된다.
	 * 
	 * @param document
	 *            {@link Document} 객체
	 * @param filename
	 *            {@link Document}를 저장할 파일명
	 */
	public static void printXML(Document document, String filename) {
		OutputStream output = null;
		try {
			output = new FileOutputStream(filename);
			try {
				XMLSerializer.create(output).serialize(document);
			} finally {
				IOUtils.closeQuietly(output);
			}
		} catch (Exception except) {
			LOG.debug("file not found " + filename);
		} finally {
			// for Klocwork
			if (output != null) {
				try {
					output.close();
				} catch (IOException ioe) {
					ioe.getMessage();
				}
			}
		}
	}

	/**
	 * {@link Document}를 {@link StringBuffer}로 출력한다. 저장되는 XML의 인코딩은 기본 값인 utf-8로
	 * 설정된다.
	 * 
	 * @param document
	 *            {@link Document} 객체
	 * @return StringBuffer
	 */
	public static StringBuffer printXML(Document document) {
		StringWriter writer = new StringWriter();
		XMLSerializer.create(writer).serialize(document);
		return writer.getBuffer();
	}

	/**
	 * apache 의 xmlParser 에서 java-encodings 형식을 지원하도록 설정
	 * 
	 * @param docBuilderFactory
	 *            DocumentBuilderFactory
	 */
	private static void setAllowJavaEncoding(
			DocumentBuilderFactory docBuilderFactory) {
		String docBuilderFactoryClass = docBuilderFactory.getClass().getName();

		if (docBuilderFactoryClass.startsWith("org.apache.xerces")) {
			try {
				docBuilderFactory.setFeature(
						"http://apache.org/xml/features/allow-java-encodings",
						true);
			} catch (ParserConfigurationException except) {
				LOG.info(
						"feature(allow-java-encodings) is not supported, caused by "
								+ except, except);
			} catch (AbstractMethodError error) {
				LOG.info("feature(allow-java-encodings) is not supported. Use xerces 2.8 or above");
			}
		} else {
			LOG.debug("XML Parser is not xerces(" + docBuilderFactoryClass
					+ ")");
		}
	}

	// -- deprecated ----------------------------------------------------------

	/**
	 * {@link XPathAPI}를 활용한 {@link Node}를 검색한다.
	 * 
	 * @param element
	 *            노드 검색을 시작할 {@link Element}
	 * @param key
	 *            검색할 노드에 대한 XPath
	 * @return selected Node
	 */
	@Deprecated
	public static Node getNode(Node element, String key) {
		if (element == null) {
			return null;
		}

		Node selected = null;

		try {
			CachedXPathAPI xpathAPI = getXPathApi(element);
			synchronized (xpathAPI) {
				selected = xpathAPI.selectSingleNode(element, key);
			}
		} catch (TransformerException e) {
			LOG.error(key + " node is not searched!!");
		}

		return selected;
	}

	/**
	 * {@link XPathAPI}를 활용한 {@link NodeList}를 검색한다.
	 * 
	 * @param element
	 *            노드 검색을 시작할 {@link Element}
	 * @param key
	 *            검색할 노드에 대한 XPath
	 * @return NodeList
	 */
	@Deprecated
	public static NodeList getNodes(Node element, String key) {
		if (element == null) {
			return null;
		}

		try {
			CachedXPathAPI xpathAPI = getXPathApi(element);
			synchronized (xpathAPI) {
				return xpathAPI.selectNodeList(element, key);
			}
		} catch (TransformerException e) {
			LOG.error(key + " nodes is not searched!!", e);
		}

		return null;
	}

	/**
	 * Gets the node iterator.
	 * 
	 * @param element
	 *            the element
	 * @param key
	 *            the key
	 * 
	 * @return the node iterator
	 */
	public static Iterator<Node> getNodeIterator(Node element, String key) {
		return nodeIterable(getNodes(element, key)).iterator();
	}

	/**
	 * Gets the child iterator.
	 * 
	 * @param element
	 *            the element
	 * 
	 * @return the child iterator
	 */
	public static Iterator<Node> getChildIterator(Node element) {
		return nodeIterable(element.getChildNodes()).iterator();
	}

	/**
	 * {@link XPathAPI}의 검색 결과인 {@link Node} 리스트에 대한 {@link Iterable}을 반환한다.
	 * 
	 * @param element
	 *            노드 검색을 시작할 {@link Element}
	 * @param key
	 *            검색할 노드에 대한 XPath
	 * @return Iterable<Node>
	 */
	public static Iterable<Node> getNodeIterable(Node element, String key) {
		return nodeIterable(getNodes(element, key));
	}

	/**
	 * 주어진 {@link Element} 하위의 경로에 속한 값을 반환한다.
	 * 
	 * @param element
	 *            Node
	 * @param key
	 *            String
	 * @return String
	 */
	@Deprecated
	public static String getValue(Node element, String key) {
		return XMLDOMUtil.evaluate(XMLDOMUtil.getNode(element, key));
	}

	/**
	 * 주어진 {@link Element} 하위의 경로에 속한 값을 반환한다.
	 * 
	 * @param element
	 *            Node
	 * @param key
	 *            String
	 * @param defaultValue
	 *            String
	 * @return value String
	 */
	@Deprecated
	public static String getValue(Node element, String key, String defaultValue) {
		String value = XMLDOMUtil.evaluate(XMLDOMUtil.getNode(element, key));

		if (StringUtils.isEmpty(value)) {
			value = defaultValue;
		}

		return value;
	}

	/**
	 * 주어진 {@link Object}의 값을 반환한다.
	 * <p>
	 * {@link Object}는 아래의 Type 중 하나이며, 다른 Type일 경우 null을 반환한다.
	 * <ul>
	 * <li>{@link Element}</li>
	 * <li>{@link Attr}</li>
	 * <li>{@link Comment}</li>
	 * <li>{@link ProcessingInstruction}</li>
	 * </ul>
	 * 
	 * @param selected
	 *            Object
	 * @return result String
	 */
	@Deprecated
	public static String evaluate(Object selected) {
		if (selected == null) {
			return null;
		}

		String result = null;

		if (selected instanceof Element) {
			result = getElementText((Element) selected);
		} else if (selected instanceof Attr) {
			result = ((Attr) selected).getValue();
		} else if (selected instanceof Comment) {
			result = ((Comment) selected).getNodeValue();
		} else if (selected instanceof ProcessingInstruction) {
			result = ((ProcessingInstruction) selected).getNodeValue();
		}

		return result;
	}

	/**
	 * {@link Element}에 대한 값을 추출한다.
	 * 
	 * @param element
	 *            Node
	 * @return String
	 */
	@Deprecated
	public static String getElementText(Node element) {
		if (element.getFirstChild() == null) {
			return "";
		}

		// 하나의 element( Text or CDATA)만 존재할경우 해당 값을 추출후 반환
		if (element.getChildNodes().getLength() == 1) {
			Object obj = element.getChildNodes().item(0);

			if (obj instanceof org.w3c.dom.Text) {
				return ((Text) obj).getNodeValue();
			} else {
				return "";
			}
		}

		// 하나 이상의 element가 존재할경우 하위노드 순환하면서 값을 생성함
		StringBuffer textContent = new StringBuffer();
		boolean hasText = false;

		for (int i = 0; i < element.getChildNodes().getLength(); i++) {
			Object obj = element.getChildNodes().item(i);

			if (obj instanceof Text) {
				textContent.append(((Text) obj).getNodeValue());
				hasText = true;
			}
		}

		if (!hasText) {
			return "";
		}

		return textContent.toString();
	}

	/**
	 * {@link Node} 하위에 존재하는 param 요소 정보를 추출한다.
	 * 
	 * @param parentNode
	 *            params를 하위로 가지는 상위 노드
	 * @param namespace
	 *            노드 Namespace
	 * @return params Map<String, String>
	 */
	@Deprecated
	public static Map<String, String> getParams(Node parentNode,
			String namespace) {

		Map<String, String> params = new HashMap<String, String>();

		String xpath = StringUtils.isNotEmpty(namespace) ? namespace
				+ ":params/" + namespace + ":param" : "params/param";

		NodeList paramNodes = XMLDOMUtil.getNodes(parentNode, xpath);

		if (paramNodes == null || paramNodes.getLength() == 0) {
			xpath = StringUtils.isNotEmpty(namespace) ? namespace + ":param"
					: "param";
			paramNodes = XMLDOMUtil.getNodes(parentNode, xpath);
		}

		if (paramNodes == null || paramNodes.getLength() == 0) {
			return params; // not sure to use Collections.emptyMap
		}

		for (int subIdx = 0; subIdx < paramNodes.getLength(); subIdx++) {
			Node paramNode = paramNodes.item(subIdx);
			String propName = XMLDOMUtil.getValue(paramNode, "@name");
			String propValue = XMLDOMUtil.getValue(paramNode, "@value");

			if (propValue == null) {
				propValue = XMLDOMUtil.evaluate(paramNode);
			}

			params.put(propName, propValue);
		}

		return params;
	}

	/**
	 * {@link NodeList}에 속하는 {@link Node}들의 snapshot을 List 형태로 반환한다.
	 * 
	 * @param list
	 *            NodeList
	 * @return nodeList List<Node>
	 */
	public static List<Node> nodeList(NodeList list) {
		List<Node> nodeList = new ArrayList<Node>(list.getLength());

		for (Node node : nodeIterable(list)) {
			nodeList.add(node);
		}

		return nodeList;
	}

	/**
	 * {@link NodeList}에 대한 {@link Iterable}를 반환한다.
	 * 
	 * @param list
	 *            NodeList
	 * @return Iterable<Node>
	 */
	public static Iterable<Node> nodeIterable(final NodeList list) {
		return new Iterable<Node>() {

			public Iterator<Node> iterator() {
				return list == null ? Collections.<Node> emptyList().iterator()
						: nodeIterator(list);
			}

		};
	}

	/**
	 * {@link NodeList}에 대한 {@link Iterator}를 반환한다.
	 * 
	 * @param list
	 *            NodeList
	 * @return Iterator<Node>
	 */
	public static Iterator<Node> nodeIterator(final NodeList list) {
		return list == null ? Collections.<Node> emptyList().iterator()
				: new Iterator<Node>() {
					private int counter;

					public boolean hasNext() {
						return counter < list.getLength();
					}

					public Node next() {
						return list.item(counter++);
					}

					public void remove() {
						throw new UnsupportedOperationException("remove");
					}

				};
	}

	/**
	 * {@link Document} 에 대한 {@link CachedXPathAPI} 반환.
	 * 
	 * @param element
	 *            Node
	 * @return xpathApi CacheXPathAPI
	 */
	private static CachedXPathAPI getXPathApi(Node element) {

		Document document = null;
		if (element.getOwnerDocument() != null) {
			document = new com.nadostar.extensions.util.Document(
					element.getOwnerDocument());
		} else {
			if (element instanceof Document) {
				document = new com.nadostar.extensions.util.Document(
						(Document) element);
			} else if (element instanceof com.nadostar.extensions.util.Document) {
				document = (com.nadostar.extensions.util.Document) element;
			} else {
				throw new IllegalArgumentException();
			}
		}

		CachedXPathAPI xpathApi = CACHED_XPATH_APIS.get(document);

		if (xpathApi == null) {
			LOG.debug("create new XPath context for " + document);
			xpathApi = new CachedXPathAPI();
			CACHED_XPATH_APIS.put(document, xpathApi);
		}

		return xpathApi;
	}

	/**
	 * {@link Document}에 대한 {@link CachedXPathAPI}를 제거한다.
	 * 
	 * @param document
	 *            Document
	 * @return boolean
	 */
	public static boolean destroyDocument(Document document) {
		return CACHED_XPATH_APIS.remove(document) != null;
	}
}